package WebPAC::Store;

use warnings;
use strict;

use base 'WebPAC::Common';
use Storable;
use File::Path;
use Data::Dump qw/dump/;

=head1 NAME

WebPAC::Store - Store WebPAC data on disk

=head1 VERSION

Version 0.14

=cut

our $VERSION = '0.14';

=head1 SYNOPSIS

This module provides disk storage for normalised data and lookups.

It is one of newer components of WebPAC, so it will change from time to
time.

I will try to keep backward compatiblity by providing multiple back-ends,
but this can't be garanteed. In other words, don't delete your input
databases just yet :-)

This has additional advantage. I can create single place to plugin other
file formats which provide better performance for particular type of data.

For now, this is a prototype version.

    use WebPAC::Store;

    my $store = WebPAC::Store->new();
    ...

=head1 FUNCTIONS

=head2 new

Create new normalised database object

  my $store = new WebPAC::Store(
  	path => '/path/to/cache/ds/',
	database => 'name',
	read_only => 1,
  );

Optional parameter C<path> defines path to directory
in which cache file for C<data_structure> call will be created.

If called with C<read_only> it will not disable caching if
called without write permission (but will die on C<save_ds>).

Optional parametar C<database> will be used used as subdirectory in path if no
database in specified when calling other functions.

=cut

sub new {
	my $class = shift;
 	my $self = {@_};
	bless($self, $class);

	my $log = $self->_get_logger();

	foreach my $p (qw/path/) {
		$log->logconfess("need $p") unless ($self->{$p});
	}

	$self->path( $self->{'path'} );

	$self ? return $self : return undef;
}

=head2 path

Check if specified cache directory exist, and if not, disable caching.

 $store->path('./cache/');

If you pass false or zero value to this function, it will disable
cacheing.

You can also call this function to get current cache path.

 my $cache_path = $store->path;

=cut

sub path {
	my $self = shift;

	my $dir = shift;
	
	return $self->{path} unless defined($dir);

	my $log = $self->_get_logger();

	if ($dir) {
		my $msg;
		if (! -e $dir) {
			if ($self->{'read_only'}) {
				$msg = "doesn't exist";
			} else {
				$log->info("creating $dir");
				mkpath $dir;
			}
		} elsif (! -d $dir) {
			$msg = "is not directory";
		} elsif (! -w $dir) {
			$msg = "not writable" unless ($self->{'read_only'});
		}

		if ($msg) {
			$log->warn("cache path $dir $msg, disabling...");
			undef $self->{'path'};
		} else {
			$log->debug("using cache dir $dir");
			$self->{'path'} = $dir;
		}
	} else {
		$log->debug("disabling cache");
		undef $self->{'path'};
	}
}

=head2 load_ds

Retrive from disk one data_structure records usually using field 000 as key

  my $ds = $store->load_ds(
		database => 'ps',
		input => 'name',
		id => 42,
  );

This function will also perform basic sanity checking on returned
data and disable caching if data is corrupted (or changed since last
update).

C<input> is used to differenciate different source input databases
which are indexed in same database.

C<database> if B<optional> argument which will override database name used when creating
C<WebPAC::Store> object (for simple retrival from multiple databases).

Returns hash or undef if cacheing is disabled or unavailable.

=cut

sub load_ds {
	my $self = shift;

	my $log = $self->_get_logger;

	my $cache_path = $self->{'path'};

	if (! $cache_path) {
		$log->warn("path not set, ignoring load_ds");
		return;
	}

	$log->logconfess("arguments for load_ds must be HASH") unless (ref(\@_) eq 'ARRAY' && ($#_ % 2 == 1));

	my $args = {@_};
	my $id = $args->{id};

	$log->logconfess("got hash, but without id") unless (defined($id));
	$log->logconfess("got hash, but id [$id] isn't number") unless ($id =~ /^\d+$/);

	my $database = $args->{database} || $self->{database} || $log->logconfess("no database?");

	my $input = $args->{input} || '';

	my $cache_file = "$cache_path/ds/$database/$input/$id";
	$cache_file =~ s#//#/#go;

	$log->debug("using cache_file $cache_file");

	if (-r $cache_file) {
		my $ds_ref = retrieve($cache_file);
		if ($ds_ref) {
			$log->debug("cache hit: $cache_file");
			if ($ds_ref->{'ds'}) {
				return $ds_ref->{'ds'};
			} else {
				$log->warn("cache entry $cache_file corrupt. Use rm $cache_path/* to re-create it on next run!");
				undef $self->{'path'};
			}
		}
	} else {
		#$log->warn("cache entry $cache_file doesn't exist");
		return undef;
	}

	return undef;
}

=head2 save_ds

Store data_structure on disk.

  $store->save_ds(
  	database => 'name',
	input => 'name',
  	id => $ds->{000}->[0],
	ds => $ds,
  );

C<database> and C<input> are optional.

=cut

sub save_ds {
	my $self = shift;

	die "can't write to database in read_only mode!" if ($self->{'read_only'});

	return unless($self->{'path'});

	my $args = {@_};

	my $log = $self->_get_logger;
	$log->debug("save_ds arguments:", dump( \@_ ));

	foreach my $f (qw/id ds/) {
		$log->logconfess("need $f") unless (defined($args->{$f}));
	}

	my $database = $args->{database} || $self->{database};
	$log->logconfess("can't find database name") unless (defined($database));

	my $input = $args->{input} || '';

	my $cache_file = $self->{path} . "/ds/$database/$input/";
	$cache_file =~ s#//#/#go;

	mkpath($cache_file) unless (-d $cache_file);

	$cache_file .= $args->{id};

	$log->debug("creating storable cache file $cache_file");

	return store {
		ds => $args->{ds},
		id => $args->{id},
	}, $cache_file;

}

=head2 load_lookup

Loads lookup hash from file

  $data = $store->load_lookup(
  	database => $database,
	input => $input,
	key => $key,
  );

C<database> is optional.

=cut

sub load_lookup {
	my $self = shift;
	my $args = {@_};

	my $log = $self->_get_logger;

	foreach my $r (qw/input key/) {
		$log->logconfess("need '$r'") unless defined($args->{$r});
	}

	my $database = $args->{database} || $self->{database} || $log->logconfess("no database?");

	my $path = $self->{path} . "/lookup/$database/" . $args->{input} . '/' . $args->{key};

	if (! -e $path) {
		$log->error("lookup $path doesn't exist, lookups will be disabled. Try re-indexing $database/", $args->{input});
		return;
	}

	if (my $data = retrieve($path)) {
		$log->info("loaded lookup $path");
		return $data;
	} else {
		$log->logwarn("can't load lookup $database/", $args->{input}, "/", $args->{key}, " from $path: $!");
		return undef;
	}
}

=head2 save_lookup

Save lookup data to file.

  $store->save_lookup(
  	database => $database,
	input => $input,
	key => $key,
	data => $lookup,
  );

C<database> is optional.

=cut

sub save_lookup {
	my $self = shift;
	my $args = {@_};

	my $log = $self->_get_logger;

	foreach my $r (qw/input key data/) {
		$log->logconfess("need '$r'") unless defined($args->{$r});
	}

	my $database = $args->{database} || $self->{database} || $log->logconfess("no database?");

	my $path = $self->{path} . "/lookup/$database/" . $args->{input};

	mkpath($path) unless (-d $path);

	$path .= "/" . $args->{key};

	my $t = time();

	if (store $args->{data}, $path) {
		$log->info(sprintf("saved lookup $path in %.2fs", time() - $t));
		return 1;
	} else {
		$log->logwarn("can't save lookup to $path: $!");
		return undef;
	}
}

=head2 load_row

Loads row from input database cache (used for lookups)

  $row = $store->load_row(
  	database => $database,
	input => $input,
	id => 42,
  );

C<database> is optional.

=cut

sub load_row {
	my $self = shift;
	my $args = {@_};

	my $log = $self->_get_logger;

	foreach my $r (qw/input id/) {
		$log->logconfess("need '$r'") unless defined($args->{$r});
	}

	my $database = $args->{database} || $self->{database} || $log->logconfess("no database?");

	my $path = $self->{path} . "/row/$database/" . $args->{input} . '/' . $args->{id};

	if (! -e $path) {
		$log->warn("input row $path doesn't exist, skipping");
		return;
	}

	if (my $data = retrieve($path)) {
		$log->debug("loaded row $path");
		return $data;
	} else {
		$log->logwarn("can't load row from $path: $!");
		return undef;
	}
}

=head2 save_row

Save row data to file.

  $store->save_row(
  	database => $database,
	input => $input,
	id => $mfn,
	row => $lookup,
  );

C<database> is optional.

=cut

sub save_row {
	my $self = shift;
	my $args = {@_};

	my $log = $self->_get_logger;

	foreach my $r (qw/input id row/) {
		$log->logconfess("need '$r'") unless defined($args->{$r});
	}

	my $database = $args->{database} || $self->{database} || $log->logconfess("no database?");

	my $path = $self->{path} . "/row/$database/" . $args->{input};

	mkpath($path) unless (-d $path);

	$path .= "/" . $args->{id};

	if (store $args->{row}, $path) {
		$log->debug("saved row $path");
		return 1;
	} else {
		$log->logwarn("can't save row to $path: $!");
		return undef;
	}
}


=head1 AUTHOR

Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2005-2006 Dobrica Pavlinusic, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1; # End of WebPAC::Store
